/*
matSize is the elements numbers of one row in matrix, and assume that the width of a element is 1 byte
mem_data is from DDR, which is assumed to be 64 bytes width
mem_valid is a one bit control signal from the controller, indicating that the data from DDR is valid;
advance_fifo signals to drop the tile at the end of the FIFO and advance everythin forward

output:
tile: entire tile at the front of the queuq(8*matSize*matSize)
ready: the tile output is valid 
full: there is no room in FIFO
*/

package MYFIFO
import chisel3._
import chisel3.util._


class MYFIFO(matSize:Int, memDataWidth:Int=64, dataWidth:Int=8) extends Module{
	val io = IO(new Bundle{
		val memData = Input(UInt((memDataWidth*8).W))
		val memValid = Input(Bool())
		val advance_fifo = Input(Bool())

		val tile = Output(Vec(matSize, UInt((dataWidth*matSize).W)))
		val ready = Output(Bool())
		val full = Output(Bool())
	})

	val totalSize = matSize * matSize * dataWidth/8 //total size of a tile in byte, assume that the byte size of element in matrix is 1;
	val tileSize = totalSize * 8 // total size of a tile in bit
	val size = log2Up(totalSize/memDataWidth)

	val state = RegInit(0.U(size.W))
	val startup = RegNext(true.B, false.B)

	//declare top row of buffer: need to write it in memDataWidth-byte chunks
	val maxNum = (totalSize+memDataWidth)/memDataWidth
	val topBuf = RegInit(VecInit(Seq.fill(maxNum)(0.U((memDataWidth*8).W))))//create maxNum memDataWidth*8-bit width registers, topBuf(0) is the LSB

	//save the command used to advance FIFO, because it may not complete immediately
	val droptile = RegInit(false.B)//only if it is true, means buf2 ~ buf4 are empty
	val clearDrop = WireDefault(false.B)//only if it is true, means the drop tile operation is finished, set droptile as false

	when(io.advance_fifo){//only when io.advance_fifo is true, set the droptile as true, then droptile will cause the
		droptile := true.B//droptile must be true after the empty4 is false
	} .elsewhen(clearDrop) {//the clearDrop will be true after the drop operation is finished
		droptile := false.B
	}

	//when the data from ddr is valid, write the data into the next buffer space
	when(io.memValid && ~full){//the io.memValid must not be asssert when the io.full is true
		when(state<(maxNum-1).U){
			state := state + 1.U
		} .otherwise{
			state := 0.U
		}
		//topBuf(maxNum-1-state.toInt) := io.memData
		topBuf(state) := io.memData
	}

	//check if the first space is filled and when data moves out of it
	val full = RegInit(false.B)
	val clearTop = WireDefault(false.B)
	when(io.memValid && state === (maxNum-1).U){//when the top buff is full, set full as true
		full := true.B
	} .elsewhen(clearTop) {
		full := false.B
	}

	//build buffers for remainder of FIFO
	val buf2 = RegInit(0.U(tileSize.W))//the 2~4 buffer's width is equal to a tile's bitwidth
	val buf3 = RegInit(0.U(tileSize.W))
	val buf4 = RegInit(0.U(tileSize.W))

	val empty2 = RegInit(true.B)//reset the 2~3 buffers as empty
	val empty3 = RegInit(true.B)
	val empty4 = RegInit(true.B)

	when(~startup){//startup becomes false when reset is assert
		empty2 := true.B
		empty3 := true.B
		empty4 := true.B
	} .elsewhen(full && empty2) {//the first buffer is full, 2-nd is empty
		buf2 := topBuf.asUInt
		clearTop := true.B
		empty2 := false.B
	} .elsewhen(empty3 && ~empty2){
		buf3 := buf2
		empty3 := false.B;
		empty2 := true.B
	} .elsewhen(empty4 && ~empty3){
		buf4 := buf3
		empty4 := false.B
		empty3 := true.B
	} .elsewhen(droptile){
		empty4 := true.B
		clearDrop := true.B
	}

	io.ready := startup && (~empty4) && (~droptile)//only if (start) && (buf4 is filled) && (droptile is false)
	io.full := full
	for(i <- 0 until matSize){
		io.tile(i) := buf4((i+1)*matSize*dataWidth-1, i*matSize*dataWidth)
	}
}

object FIFOMain extends App {
  println("Generating the FIFO hardware")
  emitVerilog(new MYFIFO(3), Array("--target-dir", "generated"))
}